# coding: utf-8

import json
import math
import logging
from decimal import Decimal, InvalidOperation
from collections import defaultdict

from flask_sqlalchemy import BaseQuery

from sqlalchemy import cast, Integer, and_, func, DECIMAL, union
from sqlalchemy.orm import aliased

from app.exceptions import NotProductError, InputError
from app.products.constant import (
    CalculateItem,
    AssembleWay,
    Transport,
    SceneType,
    ColorType,
    SECTION_LENGTH,
    REDUNDANCY,
    MM_TO_M,
    SINGLE_RETICLE_PIXEL,
    InstallWay,
    TOTAL_PIXEL,
    STEEL_PRICE,
    INCREMENT,
    TypeToName,
    POWER_MAX_LOAD_RATE,
    WorkingCondition
)
from app.products.models import ProductTemplate, ProductToParam, db


def to_model_instance(model, query, get_all=False):
    if isinstance(query, BaseQuery):
        obj = query.all() if get_all else query.first()
    else:
        obj = (
            db.session.execute(query).fetchall()
            if get_all else db.session.execute(query).fetchone()
        )
        if obj:
            if get_all:
                obj = [model(**dict(each.items())) for each in obj]
            else:
                obj = model(**dict(obj.items()))
    return obj


class SimulationProduct(object):

    def __init__(self, price=0, name='', brand='', spec=''):
        self.suggest_price = price
        self.name = name
        self.brand = brand
        self.spec = spec
        self.id = None
        self.product_type_id = None
        self.spec_display = ''

    def product_price(self, user=None):
        return self.suggest_price


class ComputeBase(object):
    """
    create by ld_wu 2017-08-22
    计算模型基类
    """

    def __init__(self, quotation):
        self.price = 0
        self.quantity = 0
        self.quotation = quotation
        self.product = None
        self.spec = ''

    # 产品类型
    product_type = ''
    product_attr = ''
    necessary = True
    modifiable = True

    @property
    def to_dict(self):
        self.get_product()
        quantity = (
            self.quantity if isinstance(self.quantity, int)
            else '{0:.2f}'.format(self.quantity)
        )
        price = self.product.product_price(getattr(self.quotation, 'user', None))
        return dict(
            product_name=self.product.name,
            brand=self.product.brand,
            price='{0:.2f}'.format(price),
            is_self=getattr(self.product, 'is_self', False),
            quantity=quantity,
            spec=self.spec,
            total='{0:.2f}'.format(price * Decimal(self.quantity)),
            product_type=TypeToName.mapping.get(self.product_type),
            product_type_id=self.product.product_type_id,
            product_id=self.product.id,
            necessary=self.necessary,
            type_id=self.product_type,
            modifiable=self.modifiable,
            spec_display=self.product.spec_display
        )

    def get_product_id(self, product_type=None):
        product_type = product_type or self.product_type
        products = self.quotation.products
        return (
            products.get(product_type) or
            products.get(str(product_type))
        )

    def get_product(self):
        if self.product:
            return self.product
        product_id = self.get_product_id()
        if product_id:
            product = ProductTemplate.get_by_args(
                id=product_id,
                product_type_id=self.product_type
            )
        else:
            query = ProductTemplate.query.filter(
                ProductTemplate.product_type_id == self.product_type
            ).order_by(
                ProductTemplate.weight.desc(), ProductTemplate.created_at.desc()
            )
            query = self.filter(query)
            product = to_model_instance(ProductTemplate, query)
        if not product:
            raise NotProductError(self.product_type, self.error_msg)
        if self.product_attr:
            setattr(self.quotation, self.product_attr, self)
        self.set_attr(product)
        self.product = product
        return product

    @property
    def error_msg(self):
        product_type = TypeToName.mapping.get(self.product_type)
        return '产品库没有找到匹配{}'.format(product_type)

    def filter(self, query):
        return query

    def set_attr(self, product):
        pass

    def option_values(self):
        query = ProductTemplate.query.filter(
            ProductTemplate.product_type_id == self.product_type
        ).order_by(
            ProductTemplate.brand
        )
        query = self.filter(query)
        options = to_model_instance(ProductTemplate, query, True)
        return [dict(
            product_id=each.id,
            product_name=each.name,
            brand=each.brand,
            price='{0:.2f}'.format(
                each.product_price(getattr(self.quotation, 'user', None))
            ),
            is_self=getattr(each, 'is_self', False),
            spec_display=each.spec_display
        ) for each in options]


class ModuleGroup(ComputeBase):

    product_type = TypeToName.MODULE_GROUP
    product_attr = 'module_group'

    def __init__(self, quotation):
        # 模组高
        self.high = 0
        # 模组宽
        self.wide = 0
        # 模组宽边分辨率
        self.w_resolution = 0
        # 模组高边分辨率
        self.h_resolution = 0
        # 模组额定功率
        self.power_rating = 0
        # 模组高边个数
        self.high_count = 0
        # 模组宽边个数
        self.wide_count = 0
        super(ModuleGroup, self).__init__(quotation)

    def set_attr(self, product):
        m_high = 0
        m_wide = 0
        rating = 0
        w_resolution = 0
        h_resolution = 0
        # 获取宽高等计算项
        product_specs = ProductToParam.query.filter(
            ProductToParam.product_template_id == product.id,
            ProductToParam.calculate_item != CalculateItem.NIL
        )

        for each in product_specs:
            if each.calculate_item == CalculateItem.WIDE:
                m_wide = Decimal(each.value)
            elif each.calculate_item == CalculateItem.HIGH:
                m_high = Decimal(each.value)
            elif each.calculate_item == CalculateItem.POWER_RATING:
                rating = self.power_rating = Decimal(each.value)
            elif each.calculate_item == CalculateItem.W_RESOLUTION:
                w_resolution = self.w_resolution = Decimal(each.value)
            elif each.calculate_item == CalculateItem.H_RESOLUTION:
                h_resolution = self.h_resolution = Decimal(each.value)
        if not all([m_wide, m_high, rating, w_resolution, h_resolution]):
            # 模组没有相应的计算项
            raise NotProductError(
                self.product_type,
                '模组-{}没有相应的计算项'.format(product.name)
            )
        self.high = m_high
        self.wide = m_wide
        self.high_count = math.ceil(self.quotation.high / self.high)
        self.wide_count = math.ceil(self.quotation.wide / self.wide)
        self.quotation.screen_high = self.high_count * self.high
        self.quotation.screen_wide = self.wide_count * self.wide
        self.quantity = self.high_count * self.wide_count
        self.spec = '{}mm * {}mm'.format(m_wide, m_high)

    def filter(self, query):
        scene_type = self.quotation.scene_type
        color = self.quotation.color
        spacing = self.quotation.spacing
        # 户内/户外
        if scene_type:
            scene_type_value = SceneType.mapping.get(scene_type)
            scene_type_model = aliased(ProductToParam)
            query = query.join(
                scene_type_model,
                ProductTemplate.id == scene_type_model.product_template_id
            ).filter(
                scene_type_model.calculate_item == CalculateItem.SCENE,
                scene_type_model.value == scene_type_value
            )
        # 全彩/单色/双色
        if color:
            color_value = ColorType.mapping.get(color)
            color_model = aliased(ProductToParam)
            query = query.join(
                color_model,
                ProductTemplate.id == color_model.product_template_id
            ).filter(
                color_model.calculate_item == CalculateItem.COLOR,
                color_model.value == color_value
            )

        # 像素间距
        if spacing:
            query = query.join(
                ProductToParam, ProductTemplate.id == ProductToParam.product_template_id
            ).filter(
                ProductToParam.calculate_item == CalculateItem.SPACING,
                cast(ProductToParam.value, DECIMAL) <= Decimal(spacing)
            )

        # 如果客户已选定租赁箱体或者原装箱体，则需要匹配箱体的模组
        box_id = self.get_product_id(TypeToName.RENTAL_BOX)
        box_id = box_id or self.get_product_id(TypeToName.COMMON_BOX)
        if box_id:
            box_wide = box_high = 0
            product_specs = ProductToParam.query.filter(
                ProductToParam.product_template_id == box_id,
                ProductToParam.calculate_item.in_(
                    [CalculateItem.WIDE, CalculateItem.HIGH]
                )
            )
            for each in product_specs:
                if each.calculate_item == CalculateItem.WIDE:
                    box_high = int(each.value)
                elif each.calculate_item == CalculateItem.HIGH:
                    box_wide = int(each.value)

            if box_wide and box_high:
                wide = aliased(ProductToParam)
                high = aliased(ProductToParam)
                query = query.join(
                    wide,
                    and_(
                        ProductTemplate.id == wide.product_template_id,
                        and_(
                            wide.calculate_item == CalculateItem.WIDE,
                            func.mod(
                                box_wide,
                                wide.value
                            ) == 0
                        )
                    )
                ).join(
                    high,
                    and_(
                        ProductTemplate.id == high.product_template_id,
                        and_(
                            high.calculate_item == CalculateItem.HIGH,
                            func.mod(
                                box_high,
                                high.value
                            ) == 0
                        )
                    )
                )

        return query

    @property
    def error_msg(self):
        quotation = self.quotation
        return '没有找到{scene}P{spacing}{color}的模组'.format(
            scene=SceneType.mapping.get(quotation.scene_type),
            spacing=quotation.spacing,
            color=ColorType.mapping.get(quotation.color)
        )


class BoxBase(ComputeBase):

    def set_quantity(self):
        pass

    def set_attr(self, product):
        product_specs = ProductToParam.query.filter(
            ProductToParam.product_template_id == product.id,
            ProductToParam.calculate_item.in_(
                [CalculateItem.WIDE, CalculateItem.HIGH]
            )
        )
        for each in product_specs:
            if each.calculate_item == CalculateItem.WIDE:
                self.box_high = int(each.value)
            elif each.calculate_item == CalculateItem.HIGH:
                self.box_wide = int(each.value)
        self.set_quantity()

    def filter(self, query):
        wide = aliased(ProductToParam)
        query = query.join(
            wide,
            and_(
                ProductTemplate.id == wide.product_template_id,
                and_(
                    wide.calculate_item == CalculateItem.WIDE,
                    func.mod(wide.value, self.quotation.module_group.wide) == 0
                )
            )
        ).join(
            ProductToParam,
            and_(
                ProductTemplate.id == ProductToParam.product_template_id,
                and_(
                    ProductToParam.calculate_item == CalculateItem.HIGH,
                    func.mod(
                        ProductToParam.value, self.quotation.module_group.high
                    ) == 0
                )
            )
        )
        return query

    @property
    def error_msg(self):
        quotation = self.quotation
        product_type = TypeToName.mapping.get(self.product_type)
        return '没有找到{scene}P{spacing}{color}的{product_type}'.format(
            scene=SceneType.mapping.get(quotation.scene_type),
            spacing=quotation.spacing,
            color=ColorType.mapping.get(quotation.color),
            product_type=product_type
        )


class CommonBox(BoxBase):

    product_type = TypeToName.COMMON_BOX
    product_attr = 'box'

    def __init__(self, quotation):
        # 普通箱体a宽边个数
        self.a_wide_count = 0
        # 普通箱体a高边个数
        self.a_high_count = 0
        # 箱体b宽
        self.b_wide = 0
        # 箱体c高
        self.c_high = 0
        # 箱体高
        self.box_high = 0
        # 箱体宽
        self.box_wide = 0
        super(CommonBox, self).__init__(quotation)

    @staticmethod
    def round_six(value, base=1):
        """
        基于基数的五舍六入
        4, 1.2 = round_six(5.2, 1)
        5, 0.6 = round_six(5.6, 1)
        :return:
        """
        integral, leave = divmod(value, base)
        # 五舍六入
        if (leave / base) > 0.5:
            return integral, leave
        return integral - 1, leave + base

    def set_quantity(self):
        self.a_wide_count, self.b_wide = self.round_six(
            self.quotation.screen_wide, self.box_wide
        )
        self.a_high_count, self.c_high = self.round_six(
            self.quotation.screen_high, self.box_high
        )

        self.quantity = (
            self.quotation.screen_wide *  # 毫米
            self.quotation.screen_high *  # 毫米
            MM_TO_M  # 需要转换为平方米
        )

    @property
    def a_module_count(self):
        """
        A类箱体模组个数
       （箱体宽/模组宽）* (箱体高/模组高)
        :return:
        """
        return (
            (self.box_high / self.quotation.module_group.high) *
            (self.box_wide / self.quotation.module_group.wide)
        )

    @property
    def b_module_count(self):
        """
        B类箱体模组个数
        （箱体B宽/模组宽） * (箱体A高/模组高)
        :return:
        """
        return (
            (self.b_wide / self.quotation.module_group.wide) *
            (self.box_high / self.quotation.module_group.high)
        )

    @property
    def c_module_count(self):
        """
        C类箱体模组个数
        （箱体宽A/模组宽）* (箱体c高/模组高)
        :return:
        """
        return (
            (self.c_high / self.quotation.module_group.high) *
            (self.box_wide / self.quotation.module_group.wide)
        )

    @property
    def d_module_count(self):
        """
        D类箱体模组个数
        (箱体B宽/模组宽) * (箱体C高/模组高)
        :return:
        """
        return (
            (self.b_wide / self.quotation.module_group.wide) *
            (self.c_high / self.quotation.module_group.high)
        )

    def power_supply_quantity(self, power_obj):
        """
        普通箱体电源及电源线数量
        :return:
        """
        power_rating = power_obj.power_rating
        max_load_rate = POWER_MAX_LOAD_RATE
        module_power_rating = self.quotation.module_group.power_rating
        # A类箱体总电源数
        math.ceil(power_rating * max_load_rate / module_power_rating)
        a_count = math.ceil(
            self.a_module_count /
            math.ceil(power_rating * max_load_rate / module_power_rating)
        ) * self.a_high_count * self.a_wide_count
        # B类箱体总电源数
        b_count = math.ceil(
            self.b_module_count /
            math.ceil(power_rating * max_load_rate / module_power_rating)
        ) * self.a_high_count
        # C类箱体总电源数
        c_count = math.ceil(
            self.c_module_count /
            math.ceil(power_rating * max_load_rate / module_power_rating)
        ) * self.a_wide_count
        # D类箱体总电源数
        d_count = math.ceil(
            self.d_module_count /
            math.ceil(power_rating * max_load_rate / module_power_rating)
        )
        return a_count + b_count + c_count + d_count


class RentalBox(BoxBase):
    """
    create by ld_wu 2017-08-22
    """
    product_type = TypeToName.RENTAL_BOX
    product_attr = 'box'

    def __init__(self, quotation):
        # 箱体宽
        self.box_wide = 0
        # 箱体高
        self.box_high = 0
        super(RentalBox, self).__init__(quotation)

    def set_quantity(self):
        self.quotation.module_group.high_count = math.ceil(
            math.ceil(
                self.quotation.high / self.box_high) *
            self.box_high / self.quotation.module_group.high
        )
        self.quotation.module_group.wide_count = math.ceil(
            math.ceil(
                self.quotation.wide / self.box_wide) *
            self.box_wide / self.quotation.module_group.wide
        )

        self.quotation.screen_high = math.ceil(
            self.quotation.high / self.box_high) * self.box_high

        self.quotation.screen_wide = math.ceil(
            self.quotation.wide / self.box_wide) * self.box_wide
        self.quantity = (
            math.ceil(self.quotation.screen_high / self.box_high) *
            math.ceil(self.quotation.screen_wide / self.box_wide)
        )

    def single_power(self):
        """
        单箱体功率
        :return:
        """
        module_group = self.quotation.module_group
        power = (
            self.box_high / module_group.high *
            self.box_wide / module_group.wide *
            module_group.power_rating
        )
        return power

    def power_supply_quantity(self, power_obj):
        """
        租赁箱体电源及电源线数量
        :return:
        """
        return (
            (self.quotation.screen_high / self.box_high) *
            (self.quotation.screen_wide / self.box_wide)
        )


class OriginalBox(ComputeBase):
    """
    create by ld_wu 2017-08-23
    原装箱体
    """
    product_type = TypeToName.ORIGINAL_BOX
    product_attr = 'box'

    def __init__(self, quotation):
        # 箱体宽
        self.box_wide = 0
        # 箱体高
        self.box_high = 0
        # 模组高边个数
        self.high_count = 0
        # 模组宽边个数
        self.wide_count = 0
        super(OriginalBox, self).__init__(quotation)

    def filter(self, query):
        scene_type = self.quotation.scene_type
        color = self.quotation.color
        spacing = self.quotation.spacing

        # 户内/户外
        if scene_type:
            scene_type_value = SceneType.mapping.get(scene_type)
            scene_type_model = aliased(ProductToParam)
            query = query.join(
                scene_type_model,
                ProductTemplate.id == scene_type_model.product_template_id
            ).filter(
                scene_type_model.calculate_item == CalculateItem.SCENE,
                scene_type_model.value == scene_type_value
            )
        # 全彩/单色/双色
        if color:
            color_value = ColorType.mapping.get(color)
            color_model = aliased(ProductToParam)
            query = query.join(
                color_model,
                ProductTemplate.id == color_model.product_template_id
            ).filter(
                color_model.calculate_item == CalculateItem.COLOR,
                color_model.value == color_value
            )

        # 像素间距
        if spacing:
            query = query.join(
                ProductToParam, ProductTemplate.id == ProductToParam.product_template_id
            ).filter(
                ProductToParam.calculate_item == CalculateItem.SPACING,
                cast(ProductToParam.value, DECIMAL) <= Decimal(spacing)
            )

        return query

    def set_attr(self, product):
        product_specs = ProductToParam.query.filter(
            ProductToParam.product_template_id == product.id,
            ProductToParam.calculate_item != CalculateItem.NIL
        )
        m_high = None
        m_wide = None
        box_h_count = None
        box_w_count = None
        power_rating = None
        w_resolution = None
        h_resolution = None
        for each in product_specs:
            if each.calculate_item == CalculateItem.WIDE:
                m_wide = Decimal(each.value)
            elif each.calculate_item == CalculateItem.HIGH:
                m_high = Decimal(each.value)
            elif each.calculate_item == CalculateItem.BOX_H_COUNT:
                box_h_count = int(each.value)
            elif each.calculate_item == CalculateItem.BOX_W_COUNT:
                box_w_count = int(each.value)
            elif each.calculate_item == CalculateItem.POWER_RATING:
                power_rating = Decimal(each.value)
            elif each.calculate_item == CalculateItem.W_RESOLUTION:
                w_resolution = Decimal(each.value)
            elif each.calculate_item == CalculateItem.H_RESOLUTION:
                h_resolution = Decimal(each.value)
        if not all(
                [m_high, m_wide, box_w_count, box_h_count, power_rating,
                    w_resolution, h_resolution
                 ]):
            # 模组没有相应的计算项
            raise NotProductError(
                self.product_type,
                '箱体-{}没有相应的计算项'.format(product.name)
            )
        # 原装箱体需要计算设置模组高宽
        self.quotation.module_group = ModuleGroup(self.quotation)
        # 模组高边个数
        self.high_count = box_h_count
        # 模组宽边个数
        self.wide_count = box_w_count
        module_group = self.quotation.module_group
        module_group.high = m_high / box_h_count
        module_group.wide = m_wide / box_w_count
        module_group.power_rating = power_rating
        module_group.h_resolution = h_resolution
        module_group.w_resolution = w_resolution
        module_group.high_count = math.ceil(
            self.quotation.high / module_group.high
        )
        module_group.wide_count = math.ceil(
            self.quotation.wide / module_group.wide
        )
        self.quotation.screen_high = math.ceil(
            self.quotation.high / m_high) * m_high

        self.quotation.screen_wide = math.ceil(
            self.quotation.wide / m_wide) * m_wide
        self.box_wide = m_wide
        self.box_high = m_high

        self.quantity = (
            math.ceil(self.quotation.screen_high / m_high) *
            math.ceil(self.quotation.screen_wide / m_wide)
        )

    @property
    def error_msg(self):
        quotation = self.quotation
        product_type = TypeToName.mapping.get(self.product_type)
        return '没有找到{scene}P{spacing}{color}的{product_type}'.format(
            scene=SceneType.mapping.get(quotation.scene_type),
            spacing=quotation.spacing,
            color=ColorType.mapping.get(quotation.color),
            product_type=product_type
        )


class BoxFastener(ComputeBase):
    """
    create by ld_wu 2017-08-23
    箱体扣件
    (箱体宽边个数 + 1) * (箱体高边个数 + 1)
    """
    product_type = TypeToName.BOX_FASTENER
    product_attr = 'box_fastener'
    modifiable = False

    def set_attr(self, product):
        if self.quotation.assemble_way == AssembleWay.COMMON:
            # 普通箱体为a箱体高宽个数各加1，计算扣件时需要
            # 箱体高宽个数再加1然后相乘，所以需要加2
            self.quantity = (
                (self.quotation.box.a_high_count + 2) *
                (self.quotation.box.a_wide_count + 2)
            )
        elif self.quotation.assemble_way == AssembleWay.RENTAL:
            self.quantity = (
                (self.quotation.screen_high / self.quotation.box.box_high + 1) *
                (self.quotation.screen_wide / self.quotation.box.box_wide + 1)
            )
        else:
            self.quantity = (
                (self.quotation.screen_high / self.quotation.box.box_high) *
                (self.quotation.screen_wide / self.quotation.box.box_wide)
            )


class PowerSupply(ComputeBase):
    """
    create by ld_wu 2017-08-23
    电源
    """
    product_type = TypeToName.POWER_SUPPLY
    product_attr = 'power_supply'

    def __init__(self, quotation):
        # 电源功率
        self.power_rating = 0
        super(PowerSupply, self).__init__(quotation)

    def set_attr(self, product):
        if not self.power_rating:
            product_spec = ProductToParam.query.filter(
                ProductToParam.product_template_id == product.id,
                ProductToParam.calculate_item == CalculateItem.POWER_RATING
            ).first()
            if not product_spec:
                raise NotProductError(
                    self.product_type,
                    '电源-{}没有匹配的计算项'.format(product.name)
                )
            self.power_rating = Decimal(product_spec.value)
        self.quantity = self.quotation.box.power_supply_quantity(self)
        self.spec = '{}w'.format(self.power_rating)
        if self.product_attr:
            setattr(self.quotation, self.product_attr, self)

    def filter(self, query):
        # 租赁箱体电源的选择
        if self.quotation.assemble_way == AssembleWay.RENTAL:
            power = self.quotation.box.single_power()
            if power <= 200 or 300 < power <= 400:
                power_rating = 200
            else:
                power_rating = 300
            query = query.join(
                ProductToParam,
                and_(
                    ProductToParam.calculate_item == CalculateItem.POWER_RATING,
                    ProductTemplate.id == ProductToParam.product_template_id
                ),
            ).filter(ProductToParam.value == power_rating)
        return query


class ReceiveCard(ComputeBase):
    """
    接收卡
    """
    product_attr = 'receive_card'
    product_type = TypeToName.RECEIVE_CARD

    def __init__(self, quotation):
        self.receive_count = 0
        self.h_pixel = 0
        self.w_pixel = 0
        super(ReceiveCard, self).__init__(quotation)

    def set_attr(self, product):
        product_specs = ProductToParam.query.filter(
            ProductToParam.product_template_id == product.id,
            ProductToParam.calculate_item != CalculateItem.NIL
        )
        high_pixel = None
        wide_pixel = None
        for each in product_specs:
            if each.calculate_item == CalculateItem.H_MAX_PIXEL:
                high_pixel = Decimal(each.value)
                self.h_pixel = high_pixel
            elif each.calculate_item == CalculateItem.W_MAX_PIXEL:
                wide_pixel = Decimal(each.value)
                self.w_pixel = wide_pixel
        if not high_pixel or not wide_pixel:
            product_type = TypeToName.mapping.get(self.product_type)
            # 没有相应的计算项
            raise NotProductError(
                self.product_type,
                '{}-{}没有相应的计算项（带载点数）'.format(
                    product_type,
                    product.name
                )
            )
        if self.product_attr:
            setattr(self.quotation, self.product_attr, self)

        module_group = self.quotation.module_group

        self.quantity = self.receive_count = (
            math.ceil(
                module_group.wide_count /
                math.ceil(wide_pixel / module_group.w_resolution)
            ) *
            math.ceil(
                module_group.high_count /
                math.ceil(high_pixel / module_group.h_resolution)
            )
        )

    def filter(self, query):
        transport = self.quotation.transport
        transport_value = Transport.mapping.get(transport)

        query = query.join(
            ProductToParam,
            ProductTemplate.id == ProductToParam.product_template_id
        ).filter(
            ProductToParam.calculate_item == CalculateItem.TRANSPORT,
            ProductToParam.value.like('%{}%'.format(transport_value))
        )
        return query


class VideoBase(ComputeBase):

    def set_attr(self, product):

        product_spec = ProductToParam.query.filter(
            ProductToParam.product_template_id == product.id,
            ProductToParam.calculate_item == CalculateItem.PIXEL
        ).first()

        if not product_spec:
            product_type = TypeToName.mapping.get(self.product_type)
            # 没有相应的计算项
            raise NotProductError(
                self.product_type,
                '{}-{}没有相应的计算项（带载点数）'.format(
                    product_type,
                    product.name
                )
            )

        total_pixel = Decimal(product_spec.value)

        module_group = self.quotation.module_group
        # 高边点数
        h_total = module_group.high_count * module_group.h_resolution
        # 宽边点数
        w_total = module_group.wide_count * module_group.w_resolution
        self.quantity = math.ceil(
            h_total * w_total / total_pixel
        )


class SendingCard(ComputeBase):
    """
    create by ld_wu 2017-08-23
    发送卡
    """
    product_attr = 'sending_card'
    product_type = TypeToName.SENDING_CARD

    def filter(self, query):
        receive_card = getattr(self.quotation, 'receive_card')
        if receive_card:
            query = query.filter(
                ProductTemplate.brand == receive_card.product.brand
            )
        return query

    def set_attr(self, product):
        product_specs = ProductToParam.query.filter(
            ProductToParam.product_template_id == product.id,
            ProductToParam.calculate_item.in_([
                CalculateItem.H_MAX_PIXEL, CalculateItem.W_MAX_PIXEL
            ])
        )
        high_pixel = None
        wide_pixel = None
        for each in product_specs:
            if each.calculate_item == CalculateItem.H_MAX_PIXEL:
                high_pixel = Decimal(each.value)
            elif each.calculate_item == CalculateItem.W_MAX_PIXEL:
                wide_pixel = Decimal(each.value)
        if not high_pixel or not wide_pixel:
            # 没有相应的计算项
            product_type = TypeToName.mapping.get(self.product_type)
            raise NotProductError(
                self.product_type,
                '{}-{}没有相应的计算项（带载点数）'.format(
                    product_type,
                    product.name
                )
            )
        module_group = self.quotation.module_group
        # 高边点数
        h_total = module_group.high_count * module_group.h_resolution
        # 宽边点数
        w_total = module_group.wide_count * module_group.w_resolution

        self.quantity = (
            math.ceil(h_total / high_pixel) *
            math.ceil(w_total / wide_pixel)
        )


class AsyncSendingCard(SendingCard):
    """
    create by ld_wu 2017-08-23
    异步发送卡
    """
    product_attr = 'sending_card'
    product_type = TypeToName.ASYNC_SENDING_CARD


class AsyncController(ComputeBase):
    """
    create by ld_wu 2017-08-23
    异步控制器
    """
    product_attr = 'async_controller'
    product_type = TypeToName.ASYNC_CONTROLLER

    def set_attr(self, product):
        def cal_card_count(cal_total, cal_pixel, o_total):
            cal_count = math.ceil(cal_total / cal_pixel)
            cal_rel_pixel = math.ceil(cal_total / cal_count)
            o_rel_pixel = math.ceil(
                total_pixel / cal_rel_pixel * Decimal('0.9'))
            w_count = math.ceil(o_total / o_rel_pixel)
            return w_count * cal_count

        product_specs = ProductToParam.query.filter(
            ProductToParam.product_template_id == product.id,
            ProductToParam.calculate_item != CalculateItem.NIL
        )
        high_pixel = None
        wide_pixel = None
        total_pixel = None
        for each in product_specs:
            if each.calculate_item == CalculateItem.H_MAX_PIXEL:
                high_pixel = Decimal(each.value)
            elif each.calculate_item == CalculateItem.W_MAX_PIXEL:
                wide_pixel = Decimal(each.value)
            elif each.calculate_item == CalculateItem.PIXEL:
                total_pixel = Decimal(each.value)
        if not high_pixel or not wide_pixel or not total_pixel:
            # 没有相应的计算项
            product_type = TypeToName.mapping.get(self.product_type)
            raise NotProductError(
                self.product_type,
                '{}-{}没有相应的计算项（带载点数）'.format(
                    product_type,
                    product.name
                )
            )

        module_group = self.quotation.module_group
        # 高边点数
        h_total = module_group.high_count * module_group.h_resolution
        # 宽边点数
        w_total = module_group.wide_count * module_group.w_resolution
        self.quantity = min(
            cal_card_count(h_total, high_pixel, w_total),
            cal_card_count(w_total, wide_pixel, h_total)
        )


class Video(VideoBase):

    product_attr = 'video'

    def __init__(self, quotation):
        module_group = quotation.module_group
        total_pixel = (
            module_group.w_resolution *
            module_group.wide_count *
            module_group.h_resolution *
            module_group.high_count
        )
        self._total_pixel = total_pixel

        if total_pixel <= TOTAL_PIXEL:
            self.product_type = TypeToName.VIDEO_PROCESSOR
        else:
            self.product_type = TypeToName.VIDEO_SPLICER
        super(Video, self).__init__(quotation)

    def filter(self, query):
        """
        create by ld_wu 2017-09-18
        带载点数≥屏体总点数
        """

        query = query.join(
            ProductToParam,
            and_(
                ProductTemplate.id == ProductToParam.product_template_id,
                ProductToParam.calculate_item == CalculateItem.PIXEL,
                cast(ProductToParam.value, Integer) >= self._total_pixel
            )
        ).with_entities(
            ProductTemplate.id.label('id'),
            ProductTemplate.name.label('name'),
            ProductTemplate.brand.label('brand'),
            ProductTemplate.product_type_id.label('product_type_id'),
            ProductTemplate.suggest_price.label('suggest_price')
        ).order_by(
            cast(ProductToParam.value, Integer),
            ProductTemplate.weight.desc(),
            ProductTemplate.created_at.desc()
        )
        product_query = ProductTemplate.query.filter(
            ProductTemplate.product_type_id == self.product_type
        ).join(
            ProductToParam,
            and_(
                ProductTemplate.id == ProductToParam.product_template_id,
                ProductToParam.calculate_item == CalculateItem.PIXEL
            )
        ).filter(
            cast(ProductToParam.value, Integer) > 0
        ).with_entities(
            ProductTemplate.id.label('id'),
            ProductTemplate.name.label('name'),
            ProductTemplate.brand.label('brand'),
            ProductTemplate.product_type_id.label('product_type_id'),
            ProductTemplate.suggest_price.label('suggest_price')
        ).order_by(
            ProductTemplate.weight.desc(), ProductTemplate.created_at.desc()
        )
        query = union(query, product_query)
        return query


class SecuritySplicer(VideoBase):
    """
    create by ld_wu 2017-09-15
    安防拼接器
    """
    product_type = TypeToName.SECURITY

    def filter(self, query):
        """
        create by ld_wu 2017-09-18
        安防拼接器带载点数≥屏体总点数
        """
        module_group = self.quotation.module_group
        total_pixel = (
            module_group.w_resolution *
            module_group.wide_count *
            module_group.h_resolution *
            module_group.high_count
        )

        query = query.join(
            ProductToParam,
            and_(
                ProductTemplate.id == ProductToParam.product_template_id,
                ProductToParam.calculate_item == CalculateItem.PIXEL,
                cast(ProductToParam.value, Integer) >= total_pixel
            )
        ).with_entities(
            ProductTemplate.id.label('id'),
            ProductTemplate.name.label('name'),
            ProductTemplate.brand.label('brand'),
            ProductTemplate.product_type_id.label('product_type_id'),
            ProductTemplate.suggest_price.label('suggest_price')
        ).order_by(
            cast(ProductToParam.value, Integer),
            ProductTemplate.weight.desc(),
            ProductTemplate.created_at.desc()
        )
        product_query = ProductTemplate.query.filter(
            ProductTemplate.product_type_id == self.product_type
        ).join(
            ProductToParam,
            and_(
                ProductTemplate.id == ProductToParam.product_template_id,
                ProductToParam.calculate_item == CalculateItem.PIXEL
            )
        ).filter(
            cast(ProductToParam.value, Integer) > 0
        ).with_entities(
            ProductTemplate.id.label('id'),
            ProductTemplate.name.label('name'),
            ProductTemplate.brand.label('brand'),
            ProductTemplate.product_type_id.label('product_type_id'),
            ProductTemplate.suggest_price.label('suggest_price')
        ).order_by(
            ProductTemplate.weight.desc(), ProductTemplate.created_at.desc()
        )
        query = union(query, product_query)
        return query


class Screw(ComputeBase):
    """
    create by ld_wu 2017-08-23
    螺丝
    """
    product_type = TypeToName.SCREW

    def set_attr(self, product):
        # 单个模组需要的螺丝数
        single_count = 12
        module_group = self.quotation.module_group
        self.quantity = (
            module_group.high_count * module_group.wide_count * single_count
        )


class SteelStructure(ComputeBase):
    """
    create by ld_wu 2017-08-23
    钢结构
    """
    product_type = TypeToName.STEEL_STRUCTURE
    necessary = False
    modifiable = False

    def __init__(self, quotation):
        self.necessary = getattr(quotation, 'is_client', False)
        super(SteelStructure, self).__init__(quotation)

    def get_product(self):
        self.set_attr(self.product)

    def set_attr(self, product):
        def steel_price():
            try:
                steel_prices = STEEL_PRICE[
                    self.quotation.scene_type][self.quotation.install_way]
                for _area, _price in steel_prices:
                    if _area is None:
                        return _price
                    elif area <= (_area * pow(10, 6)):
                        return _price
            except KeyError:
                return 0

        area = (
            (self.quotation.screen_wide + INCREMENT) *
            (self.quotation.screen_high + INCREMENT)
        )
        price = steel_price()
        self.quantity = area * MM_TO_M
        self.product = SimulationProduct(
            price=price,
            name=TypeToName.mapping.get(self.product_type, '')
        )


class SteelStructureHigh(SteelStructure):
    """
    create by ld_wu 2017-08-23
    钢结构离地高度
    """
    product_type = TypeToName.STEEL_STRUCTURE_HIGH
    necessary = False
    modifiable = False

    def __init__(self, quotation):
        self.necessary = getattr(quotation, 'is_client', False)
        super(SteelStructureHigh, self).__init__(quotation)

    def set_attr(self, product):
        quotation = self.quotation

        def get_quantity():
            return quotation.ground_high - base
        price = 0
        if quotation.install_way in [InstallWay.SINGLE, InstallWay.DOUBLE]:
            # 单立柱\双立柱的高度大于4米需要加收价格(50元/平)
            base = 4
            if quotation.ground_high > base:
                price = 50
                self.quantity = get_quantity()
        elif quotation.install_way in [InstallWay.EMBED, InstallWay.WALL]:
            # 墙挂或者嵌入的方式高于15米需要加价（10元/平）
            base = 15
            if quotation.ground_high > base:
                price = 10
                self.quantity = get_quantity()
        self.product = SimulationProduct(
            price=price,
            name=TypeToName.mapping.get(self.product_type, '')
        )


class SteelStructureWelding(SteelStructure):
    """
    create by ld_wu 2017-08-23
    钢结构加焊不锈钢
    """
    product_type = TypeToName.STEEL_STRUCTURE_WELDING
    necessary = False
    modifiable = False

    def set_attr(self, product):
        quotation = self.quotation
        area = (
            (quotation.screen_wide + INCREMENT) *
            (quotation.screen_high + INCREMENT)
        )
        self.quantity = area * MM_TO_M
        # 焊接为100元/平
        price = 100
        self.product = SimulationProduct(
            price=price,
            name=TypeToName.mapping.get(self.product_type, '')
        )


class SteelStructureToAluminum(SteelStructure):
    """
    create by ld_wu 2017-08-23
    钢结构换成铝单板
    """
    product_type = TypeToName.STEEL_STRUCTURE_TO_ALUMINUM

    def set_attr(self, product):
        quotation = self.quotation
        area = (
            (quotation.screen_wide + INCREMENT) *
            (quotation.screen_high + INCREMENT)
        )
        self.quantity = area * MM_TO_M
        # 焊接为100元/平
        price = 150
        self.product = SimulationProduct(
            price=price,
            name=TypeToName.mapping.get(self.product_type, '')
        )


class PowerLine(ComputeBase):
    """
    create by ld_wu 2017-08-23
    电源线
    """

    product_type = TypeToName.POWER_LINE
    modifiable = False

    def set_attr(self, product):
        quotation = self.quotation
        if self.quotation.assemble_way == AssembleWay.ORIGINAL:
            quantity = (
                math.ceil(quotation.screen_high / quotation.box.box_high) *
                math.ceil(quotation.screen_wide / quotation.box.box_wide)
            )
        else:
            quantity = quotation.power_supply.quantity
        self.quantity = quantity


class Cable(ComputeBase):
    """
    create by ld_wu 2017-08-23
    排线
    """

    product_type = TypeToName.CABLE
    modifiable = False

    def set_attr(self, product):
        module_group = self.quotation.module_group
        self.quantity = module_group.wide_count * module_group.high_count


class OneAndHalfReticle(ComputeBase):
    """
    create by ld_wu 2017-08-23
    箱体间网线计算模型
    """

    product_type = TypeToName.ONE_AND_HALF_M_RETICLE
    modifiable = False

    def set_attr(self, product):
        module_group = self.quotation.module_group
        # 高边点数
        h_total = module_group.high_count * module_group.h_resolution
        # 宽边点数
        w_total = module_group.wide_count * module_group.w_resolution
        total = w_total * h_total

        if self.quotation.assemble_way == AssembleWay.ORIGINAL:
            box = self.quotation.box

            self.quantity = max(
                (box.high_count * box.wide_count -
                 math.ceil(total / SINGLE_RETICLE_PIXEL)
                 ), 0
            )
        else:
            receive_card = self.quotation.receive_card
            if receive_card:
                self.quantity = max(
                    (receive_card.receive_count -
                     math.ceil(total / SINGLE_RETICLE_PIXEL)
                     ), 0
                )


class Computer(ComputeBase):
    """
    create by ld_wu 2017-08-23
    电脑计算模型
    数量：只需要一台
    """
    product_type = TypeToName.COMPUTER
    necessary = False

    def set_attr(self, product):
        self.quantity = 1


class ExhaustFan(ComputeBase):
    """
    create by ld_wu 2017-08-23
    轴流风机计算模型
    数量：ceil(area/30) * 2 每30平方需要2台
    """
    product_type = TypeToName.EXHAUST_FAN
    necessary = False

    def set_attr(self, product):
        area = (
            self.quotation.screen_wide *
            self.quotation.screen_high *
            MM_TO_M
        )
        self.quantity = math.ceil(area / 30) * 2


class AirConditioner(ComputeBase):
    """
    create by ld_wu 2017-08-23
    空调计算模型
    小于50平要1台，大于50小于100平要2台，大于100每匹带10平
    """
    product_type = TypeToName.AIR_CONDITIONER
    necessary = False

    def set_attr(self, product):
        area = (
            self.quotation.screen_wide *
            self.quotation.screen_high *
            MM_TO_M
        )
        if area <= 50:
            self.quantity = 1
        elif area <= 100:
            self.quantity = 2
        else:
            spec = ProductToParam.get_by_args(
                product_template_id=product.id, calculate_item=CalculateItem.P
            )
            if not spec:
                product_type = TypeToName.mapping.get(self.product_type)
                raise NotProductError(
                    self.product_type,
                    '{}-{}没有相应的计算项（匹）'.format(
                        product_type,
                        product.name
                    )
                )
            self.quantity = math.ceil(area/(10 * int(spec.value)))


class PowerAmplifier(ComputeBase):
    """
    create by ld_wu 2017-08-23
    功放计算模型
    数量： 只需要一个
    """
    product_type = TypeToName.POWER_AMPLIFIER
    necessary = False

    def set_attr(self, product):
        self.quantity = 1


class SoundColumn(ComputeBase):
    """
    create by ld_wu 2017-08-23
    音柱计算模型
    数量： 只需要一个
    """
    product_type = TypeToName.SOUND_COLUMN
    necessary = False

    def set_attr(self, product):
        self.quantity = 1


class LightningRod(ComputeBase):
    """
    create by ld_wu 2017-08-23
    避雷针计算模型
    数量： 只需要一个
    """
    product_type = TypeToName.LIGHTNING_ROD
    necessary = False

    def set_attr(self, product):
        self.quantity = 1


class SectionBar(ComputeBase):
    """
    create by ld_wu 2017-08-23
    型材
    """
    product_type = TypeToName.SECTION_BAR
    product_attr = 'box'

    def set_attr(self, product):
        # 周长
        perimeter = (
            (self.quotation.screen_high + self.quotation.screen_wide) * 2
        )
        self.quantity = math.ceil(perimeter / SECTION_LENGTH)
        if self.product_attr:
            setattr(self.quotation, self.product_attr, self)

    def power_supply_quantity(self, power_obj):
        """
        普通箱体电源及电源线数量
        :return:
        """
        power_rating = power_obj.power_rating

        module_group = self.quotation.module_group
        # 电源数量
        quantity = math.ceil(
            module_group.wide_count *
            module_group.high_count *
            module_group.power_rating /
            REDUNDANCY /
            power_rating
        )
        return quantity


class SectionBarCorner(ComputeBase):
    """
    create by ld_wu 2017-08-23
    型材拐角
    数量固定为4个
    """
    product_type = TypeToName.SECTION_BAR_CORNER
    modifiable = False

    def set_attr(self, product):
        self.quantity = 4


class SectionBarInterface(ComputeBase):
    """
    create by ld_wu 2017-08-23
    型材接口计算模型
    (floor(屏宽/6000) + floor(屏高/6000)) * 2
    """
    product_type = TypeToName.SECTION_BAR_INTERFACE
    modifiable = False

    def set_attr(self, product):
        self.quantity = (
            math.floor(self.quotation.screen_wide / SECTION_LENGTH) +
            math.floor(self.quotation.screen_high / SECTION_LENGTH)
        ) * 2


class Pendant(ComputeBase):
    """
    create by ld_wu 2017-08-23
    挂件计算模型
    数量为：ceil(屏体实际宽(米)) * 2 + 2
    """
    product_type = TypeToName.PENDANT
    modifiable = False

    def set_attr(self, product):
        # 屏宽需要从毫米换算成米
        self.quantity = math.ceil(
            self.quotation.screen_wide * Decimal('0.001')
        ) * 2 + 2


class SquarePipe(ComputeBase):
    """
    方管 or 特殊方管
    """
    modifiable = False

    def __init__(self, quotation):
        if quotation.screen_high <= SECTION_LENGTH:
            self.product_type = TypeToName.SQUARE_PIPE
        else:
            self.product_type = TypeToName.SPECIAL_SQUARE_PIPE
        super(SquarePipe, self).__init__(quotation)

    def set_attr(self, product):
        if self.quotation.screen_high <= SECTION_LENGTH:
            quantity = math.ceil(
                (self.quotation.module_group.wide_count + 1) /
                math.floor(SECTION_LENGTH / self.quotation.screen_high)
            )
        else:
            quantity = self.quotation.module_group.wide_count + 1
        self.quantity = quantity


class Magnet(ComputeBase):
    """
    磁铁
    """

    product_type = TypeToName.MAGNET

    def set_attr(self, product):
        # 单个模组需要的磁铁数
        single_count = 4
        module_group = self.quotation.module_group
        self.quantity = (
            module_group.wide_count * module_group.high_count * single_count
        )


class Distributor(ComputeBase):
    """
    create by ld_wu 2017-09-22
    配电柜
    总功率大于5000则需要一台，否则不需要
    """
    product_type = TypeToName.DISTRIBUTOR
    necessary = False

    def set_attr(self, product):
        module_group = self.quotation.module_group
        total_power = (
            module_group.wide_count *
            module_group.high_count *
            module_group.power_rating
        )
        if total_power > 5000:
            self.quantity = 1


class QuotationProcess(object):
    """
    create by ld_wu 2017-08-22
    价格计算
    """

    def __init__(self, source):
        self.source = source

        # 工况
        self.working = source.working.data
        # 输入高
        self.high = Decimal(source.high.data) * 1000
        # 输入宽
        self.wide = Decimal(source.wide.data) * 1000
        # 像素间距
        self.spacing = source.spacing.data
        # 户内外
        self.scene_type = source.scene_type.data
        # 颜色
        self.color = source.color.data
        # 组装方式
        self.assemble_way = source.assemble_way.data
        # 安装方式
        self.install_way = source.install_way.data
        # 传输方式
        self.transport = source.transport.data
        # 离地高度
        self.ground_high = source.ground_high.data
        # 实际高度
        self.screen_high = 0
        # 实际宽度
        self.screen_wide = 0

        products = source.products.data
        self.products = json.loads(products) if products else dict()

        self._module_group = None
        self._box = None
        self._power_supply = None
        self.receive_card = None

    @property
    def module_group(self):
        if not self._module_group:
            raise InputError('输入条件冲突，计算过程缺少模组')
        return self._module_group

    @module_group.setter
    def module_group(self, value):
        self._module_group = value

    @property
    def box(self):
        if not self._box:
            if (self.scene_type == SceneType.OUTDOOR and
                    self.assemble_way == AssembleWay.SECTION):
                raise InputError('户外无法使用型材拼装')
            raise InputError('输入条件与组装方式存在冲突')
        return self._box

    @box.setter
    def box(self, value):
        self._box = value

    @property
    def power_supply(self):
        if not self._power_supply:
            raise InputError('输入条件冲突，计算过程缺少电源')
        return self._power_supply

    @power_supply.setter
    def power_supply(self, value):
        self._power_supply = value

    def reduce_compute(self, rules):
        """
        汇总各项计算结果
        """
        rules = sorted(rules, key=lambda x: ComputeSet.ALL.index(x))
        return [rule(self).to_dict for rule in rules]

    def log_error(self, e):
        logging.error(
            'working:{}-scene_type:{}-spacing:{}-color:{}'
            '-assemble_way:{}-install_way:{}-transport:{}-{}\n'.format(
                self.working,
                self.scene_type,
                self.spacing,
                self.color,
                self.assemble_way,
                self.install_way,
                self.transport,
                str(e)
            )
        )

    def quotation_models(self):
        """
        create_by ld_wu 2017-08-14
        报价计算模型
        """
        try:
            rules = ComputeSet().union_compute_rules(self)
            prices = self.reduce_compute(rules)
            return prices
        except (InvalidOperation, ValueError, ZeroDivisionError) as e:
            self.log_error(e)
            raise InputError('产品计算项错误')

    def client_quotation_models(self):
        """
        create by ld_wu 2017-12-04
        PC版报价计算模型，主要是为了实现
        组装方式的推荐，型材、租赁箱体->普通箱体->原装箱体
        户内,半户外,屏体面积≤30㎡，屏高≤10m，推荐使用型材；
        p3.91,p4.81，推荐使用租赁箱体;
        其他情况使用普通箱体；
        若找不到普通箱体匹配的模组，则匹配原装箱体
        若最终无法找到可匹配的显示模块()，则前端显示 "在线议价"
        参考：https://shimo.im/doc/oyeG5vk7A70B3fks?r=3K0P05/
        :return:
        """
        def find_finally_model():
            """
            以型材、租赁箱体->普通箱体->原装箱体的顺序
            计算一个最先匹配的价格模型
            :return:
            """
            try:
                return self.quotation_models()
            except NotProductError as e:
                if e.product_type in [
                    TypeToName.MODULE_GROUP,
                    TypeToName.SECTION_BAR,
                    TypeToName.COMMON_BOX,
                    TypeToName.RENTAL_BOX
                ]:
                    next_assemble_way = assemble_way_queue.get(self.assemble_way)
                    if next_assemble_way:
                        self.assemble_way = next_assemble_way
                        return find_finally_model()
                raise e

        assemble_way_queue = {
            AssembleWay.SECTION: AssembleWay.RENTAL,
            AssembleWay.RENTAL: AssembleWay.COMMON,
            AssembleWay.COMMON: AssembleWay.ORIGINAL
        }
        self.recommend_assemble_way()
        return find_finally_model()

    def recommend_assemble_way(self):
        """
        create by ld_wu 2017-12-04
        推荐组装方式
        :return:
        """
        # 小于30平米选择型材, 屏高小于等于10米
        area = 30
        high = 10
        is_section_bar = (
            self.scene_type in [SceneType.INDOOR, SceneType.HALF] and
            self.high * self.wide <= area * pow(10, 6) and
            self.high <= high * pow(10, 3)
        )
        # 间距为3.91及4.81推荐租赁箱体
        is_rental_box = self.spacing in ['3.91', '4.81']
        # 2017-12-05 增加工况为舞台租赁的直接使用租赁箱体
        if self.working == WorkingCondition.STAGE:
            self.assemble_way = AssembleWay.RENTAL
        elif is_section_bar:
            self.assemble_way = AssembleWay.SECTION
        elif is_rental_box:
            self.assemble_way = AssembleWay.RENTAL
        else:
            self.assemble_way = AssembleWay.COMMON


class ComputeSet(object):

    # 户外计算集合
    out_door_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        PowerSupply, ReceiveCard, SendingCard, AsyncSendingCard,
        Video, SecuritySplicer,
        SectionBar, SectionBarCorner, SectionBarInterface,
        AsyncController, Screw, SteelStructure, SteelStructureHigh,
        SteelStructureWelding, SteelStructureToAluminum, PowerLine,
        Cable, OneAndHalfReticle, Computer, ExhaustFan,
        AirConditioner, PowerAmplifier, SoundColumn, LightningRod
    }

    # 半户外
    half_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        SectionBar, SectionBarCorner, SectionBarInterface,
        PowerSupply, ReceiveCard, SendingCard, AsyncSendingCard,
        Video, SecuritySplicer, AsyncController, Screw,
        SteelStructureWelding, SteelStructureToAluminum, PowerLine,
        Cable, OneAndHalfReticle, Computer, ExhaustFan,
        AirConditioner, PowerAmplifier, SoundColumn,
        Magnet, Pendant, SquarePipe
    }

    # 主要用于排序
    ALL = [
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        SectionBar, SectionBarCorner, SectionBarInterface,
        PowerSupply, ReceiveCard, SendingCard, AsyncSendingCard,
        Video, SecuritySplicer,
        AsyncController, Screw, SteelStructure, SteelStructureHigh,
        SteelStructureWelding, SteelStructureToAluminum, PowerLine,
        Cable, OneAndHalfReticle, Computer, ExhaustFan,
        AirConditioner, PowerAmplifier, SoundColumn, LightningRod,
        Magnet, Pendant, SquarePipe
    ]

    # 户内/墙挂/嵌入
    in_door_compute = wall_compute = embed_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        SectionBar, SectionBarCorner, SectionBarInterface,
        PowerSupply, ReceiveCard, AsyncSendingCard,
        SendingCard, Video, AsyncController,
        Screw, SteelStructure, SteelStructureHigh, SteelStructureWelding,
        SteelStructureToAluminum, PowerLine, Cable,
        OneAndHalfReticle, Computer, ExhaustFan, AirConditioner,
        PowerAmplifier, SoundColumn, Magnet, Pendant,
        SquarePipe, SecuritySplicer
    }

    # 原装箱体计算集合
    # 移除1.5米网线 2017-09-28
    original_box_compute = {
        OriginalBox, BoxFastener, SendingCard,
        AsyncSendingCard, Video, SteelStructure,
        SteelStructureHigh, SteelStructureWelding, SteelStructureToAluminum,
        Computer, ExhaustFan, SecuritySplicer, OneAndHalfReticle,
        AirConditioner, PowerAmplifier, SoundColumn, LightningRod,
        PowerLine
    }

    # 租赁箱体计算集合
    rental_box_compute = {
        ModuleGroup, RentalBox, PowerSupply, ReceiveCard, BoxFastener,
        SendingCard, AsyncSendingCard, Video,
        Screw, SteelStructure, SteelStructureHigh,
        SteelStructureWelding, SteelStructureToAluminum, PowerLine,
        Cable, OneAndHalfReticle, Computer, ExhaustFan, AirConditioner,
        PowerAmplifier, SoundColumn, LightningRod, SecuritySplicer
    }

    # 普通箱体计算集合
    common_box_compute = {
        ModuleGroup, BoxFastener, PowerSupply, ReceiveCard, CommonBox,
        SendingCard, AsyncSendingCard, Video, Screw,
        SteelStructure, SteelStructureHigh,
        SteelStructureWelding, SteelStructureToAluminum, PowerLine,
        Cable, OneAndHalfReticle, Computer, ExhaustFan,
        PowerAmplifier, SoundColumn, LightningRod, AirConditioner
    }

    # 落地计算集合
    ground_compute = {
        ModuleGroup, OriginalBox, CommonBox, RentalBox, BoxFastener,
        PowerSupply, ReceiveCard, SendingCard, Video, SteelStructure,
        SteelStructureHigh, SteelStructureWelding, SteelStructureToAluminum,
        AsyncController, Screw, PowerLine, Cable, AsyncSendingCard,
        OneAndHalfReticle, Computer, ExhaustFan, AirConditioner, Magnet,
        PowerAmplifier, SoundColumn, LightningRod, SecuritySplicer,
        SectionBar, SectionBarCorner, SectionBarInterface, SquarePipe

    }

    # 单立柱/双立柱
    single_column_compute = double_column_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        PowerSupply, ReceiveCard, SendingCard, Video, SteelStructure,
        SteelStructureHigh, SteelStructureWelding, SteelStructureToAluminum,
        PowerLine, Screw, OneAndHalfReticle, Computer,
        PowerAmplifier, ExhaustFan, AirConditioner, SoundColumn, LightningRod,
        SecuritySplicer, AsyncSendingCard, SquarePipe, Cable,
        SectionBar, SectionBarCorner, SectionBarInterface, Magnet
    }

    # 同步
    sync_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        PowerSupply, ReceiveCard, SendingCard, Video, Screw,
        SteelStructure, SteelStructureHigh, SteelStructureWelding,
        SteelStructureToAluminum, PowerLine, Cable,
        OneAndHalfReticle, Computer, ExhaustFan, AirConditioner,
        PowerAmplifier, SoundColumn, Magnet, SectionBar, Pendant,
        SectionBarCorner, SectionBarInterface, SquarePipe,
        LightningRod, SecuritySplicer
    }

    # 异步
    async_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        PowerSupply, ReceiveCard, AsyncSendingCard, Screw, LightningRod,
        SteelStructure, SteelStructureHigh, SteelStructureWelding,
        SteelStructureToAluminum, PowerLine, Cable,
        OneAndHalfReticle, Computer, ExhaustFan, AirConditioner,
        PowerAmplifier, SoundColumn, Magnet, SectionBar, Pendant,
        SectionBarCorner, SectionBarInterface, SquarePipe
    }

    # 门头异步
    door_async_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        SectionBar, SectionBarCorner, SectionBarInterface,
        PowerSupply, SendingCard, AsyncSendingCard,
        Video, SecuritySplicer,
        AsyncController, Screw, SteelStructure, SteelStructureHigh,
        SteelStructureWelding, SteelStructureToAluminum, PowerLine,
        Cable, ExhaustFan,
        AirConditioner, PowerAmplifier, SoundColumn, LightningRod,
        Magnet, Pendant, SquarePipe
    }

    # 型材
    section_bar_compute = {
        ModuleGroup, PowerSupply, ReceiveCard, SendingCard, Video,
        AsyncController, SectionBar, SectionBarCorner, SectionBarInterface,
        Pendant, SquarePipe, Magnet, SteelStructure, SteelStructureWelding,
        SteelStructureToAluminum, PowerLine, Cable, AirConditioner,
        PowerAmplifier, Computer, OneAndHalfReticle,
        ExhaustFan, LightningRod, SecuritySplicer, AsyncSendingCard
    }

    # 安防
    security_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        SectionBar, SectionBarCorner, SectionBarInterface,
        PowerSupply, ReceiveCard, SendingCard, AsyncController,
        Screw, SteelStructure, SteelStructureHigh, SteelStructureWelding,
        SteelStructureToAluminum, PowerLine, Cable,
        OneAndHalfReticle, Computer, ExhaustFan, AirConditioner,
        PowerAmplifier, SoundColumn, LightningRod, Magnet, Pendant,
        SquarePipe, SecuritySplicer, AsyncSendingCard
    }

    # 舞台租赁
    stage_compute = {
        ModuleGroup, OriginalBox, RentalBox, BoxFastener,
        PowerSupply, ReceiveCard, SendingCard, AsyncController,
        Screw, SteelStructure, SteelStructureHigh, SteelStructureWelding,
        SteelStructureToAluminum, PowerLine, Cable,
        OneAndHalfReticle, Computer, ExhaustFan, AirConditioner,
        PowerAmplifier, SoundColumn, LightningRod, Magnet, Pendant,
        SquarePipe, Video, AsyncSendingCard
    }

    # 其他工况
    other_working_compute = {
        ModuleGroup, OriginalBox, RentalBox, CommonBox, BoxFastener,
        SectionBar, SectionBarCorner, SectionBarInterface,
        PowerSupply, ReceiveCard, SendingCard, AsyncController,
        Screw, SteelStructure, SteelStructureHigh, SteelStructureWelding,
        SteelStructureToAluminum, PowerLine, Cable,
        OneAndHalfReticle, Computer, ExhaustFan, AirConditioner,
        PowerAmplifier, SoundColumn, LightningRod, Magnet, Pendant,
        SquarePipe, Video, AsyncSendingCard
    }

    # 门头屏
    door_working_compute = {
        ModuleGroup, SectionBar, SectionBarCorner, SectionBarInterface,
        PowerSupply, AsyncController,
        SteelStructure, SteelStructureHigh, SteelStructureWelding,
        SteelStructureToAluminum, PowerLine, Cable,
        ExhaustFan, AirConditioner,
        PowerAmplifier, SoundColumn, LightningRod, Magnet, Pendant,
        SquarePipe, Video
    }

    IN_OR_OUT_MAP = {
        SceneType.INDOOR: in_door_compute,
        SceneType.OUTDOOR: out_door_compute,
        SceneType.HALF: half_compute
    }

    BOX_MAP = {
        AssembleWay.ORIGINAL: original_box_compute,
        AssembleWay.RENTAL: rental_box_compute,
        AssembleWay.COMMON: common_box_compute,
        AssembleWay.SECTION: section_bar_compute
    }

    TRANSPORT_MAP = {
        Transport.ASYNC: async_compute,
        Transport.SYNC: sync_compute,
        Transport.DOOR_ASYNC: door_async_compute
    }

    INSTALL_WAY_MAP = {
        InstallWay.WALL: wall_compute,
        InstallWay.DOUBLE: double_column_compute,
        InstallWay.SINGLE: single_column_compute,
        InstallWay.GROUND: ground_compute,
    }

    WORKING_MAP = {
        WorkingCondition.SECURITY: security_compute,
        WorkingCondition.STAGE: stage_compute,
        WorkingCondition.DOOR: door_working_compute
    }

    def union_compute_rules(self, quotation):
        """
        create_by ld_wu 2017-08-14
        从输入条件获取相应的组合计算集合
        """
        scene_compute = self.IN_OR_OUT_MAP.get(quotation.scene_type, set())
        box_compute = self.BOX_MAP.get(quotation.assemble_way, set())
        transport_compute = self.TRANSPORT_MAP.get(quotation.transport, set())
        install_compute = self.INSTALL_WAY_MAP.get(quotation.install_way, set())
        working_compute = self.WORKING_MAP.get(
            quotation.working, self.other_working_compute
        )
        return (
            scene_compute &
            box_compute &
            transport_compute &
            install_compute &
            working_compute
        )


class ApiProcess(QuotationProcess):

    def __init__(self, source, user=None, is_client=False):
        self.source = source
        self.user = user

        # 工况
        self.working = source['working']
        # 输入高
        self.high = Decimal(source['high']) * 1000
        # 输入宽
        self.wide = Decimal(source['wide']) * 1000
        # 像素间距
        self.spacing = source['spacing']
        # 户内外
        self.scene_type = source['scene_type']
        # 颜色
        self.color = source['color']
        # 组装方式
        self.assemble_way = source['assemble_way']
        # 安装方式
        self.install_way = source['install_way']
        # 传输方式
        self.transport = source['transport']
        # 离地高度
        self.ground_high = source.get('ground_high', 0)
        # 实际高度
        self.screen_high = 0
        # 实际宽度
        self.screen_wide = 0

        self.type_id = source.get('type_id')

        products = source.get('products')
        self.products = json.loads(products) if products else dict()

        self._module_group = None
        self._box = None
        self._power_supply = None
        self.receive_card = None

        self.is_client = is_client

    def get_options(self):
        """
        create by ld_wu 2017-09-18
        产品下拉框数据
        """
        try:
            rules = ComputeSet().union_compute_rules(self)
            rules = sorted(rules, key=lambda x: ComputeSet.ALL.index(x))
            rule_map = dict()
            for rule in rules:
                obj = rule(self)
                obj.get_product()
                rule_map[str(obj.product_type)] = obj
            options = rule_map[self.type_id].option_values()
            product = rule_map[self.type_id].product
            options = sorted(
                options,
                key=lambda x: self.__option_order_rule(x, product)
            )
            return options
        except (InvalidOperation, ValueError, ZeroDivisionError) as e:
            self.log_error(e)
            raise InputError('产品计算项错误')

    @staticmethod
    def __option_order_rule(item, product):
        """
        下拉产品排序规则
        推荐产品最前，推荐产品同品牌次之，其他品牌产品
        """
        return item['product_id'] != product.id, item['brand'] != product.brand

    def reduce_compute(self, rules):
        """
        汇总各项计算结果
        """
        compute_res = defaultdict(list)
        rules = sorted(rules, key=lambda x: ComputeSet.ALL.index(x))
        total = 0
        for rule in rules:
            obj = rule(self)
            info = obj.to_dict
            if obj.quantity:
                compute_res[
                    TypeToName.to_group[obj.product_type]
                ].append(info)
                if obj.necessary:
                    price = obj.product.product_price(self.user)
                    total += price * Decimal(obj.quantity)
        return compute_res, total
